<template>
  <div class="chat-message-list" ref="listEl">
    <div v-for="(m, idx) in messages" :key="m.id" :class="['msg', m.role]"
         v-show="!(streaming && m.role==='assistant' && !m.content)">
      <div class="avatar" :data-role="m.role">{{ m.role === 'user' ? '我' : 'AI' }}</div>
      <div class="bubble-wrap">
        <div
          v-if="m.role === 'assistant' && messageHasRole(m)"
          class="role-tag"
        >当前角色：{{ resolveRoleLabel(m) }}</div>
        <div class="bubble" v-html="m.role === 'assistant' ? renderAssistant(m) : formatUser(m.content)"></div>
      </div>
    </div>
    <!-- 单一流式气泡：仅当存在流并且末尾不存在非空 assistant -->
    <div v-if="streaming && !hasNonEmptyAssistantTail" class="msg assistant streaming">
      <div class="avatar" data-role="assistant">AI</div>
      <div class="bubble" v-html="streamingRendered"></div>
    </div>
    <div class="bottom-spacer" :style="{ height: spacerHeight }"></div>
  </div>
</template>
<script setup>
import { ref, watch, nextTick, computed } from 'vue'
import { renderMarkdown } from '@/utils/markdownRenderer'

const props = defineProps({
  messages: { type: Array, default: () => [] },
  streaming: { type: Boolean, default: false },
  streamingBuffer: { type: String, default: '' },
  inputOffset: { type: Number, default: 170 },
  // 保留会话级模式用于 streaming 气泡，历史气泡使用各自 meta.mode
  mode: { type: String, default: 'default' },
  roleLabel: { type: String, default: '' }
})
const spacerHeight = computed(()=> (props.inputOffset + 4) + 'px')

const listEl = ref(null)
function scrollToEnd(){
  nextTick(() => { if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight })
}

watch(() => props.messages.length, scrollToEnd)
watch(() => props.streamingBuffer, scrollToEnd)

function formatUser(text='') {
  return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br/>')
}

const showRoleTag = computed(() => props.mode && props.mode !== 'default')

function messageMode(m){
  return (m.meta && m.meta.mode) || null
}

function messageHasRole(m){
  const mm = messageMode(m)
  return !!mm && mm !== 'default'
}

function resolveRoleLabel(m){
  const mm = messageMode(m)
  if (!mm || mm === 'default') return ''
  const map = {
    default: '普通助手',
    'role:confucius': '孔子',
    'role:laozi': '老子',
    'role:libai': '李白',
    'role:dufu': '杜甫'
  }
  return map[mm] || '角色'
}

// 是否已有一个非空最终 assistant 尾部消息
const hasNonEmptyAssistantTail = computed(()=>{
  if (!props.messages.length) return false
  const last = props.messages[props.messages.length-1]
  return last.role === 'assistant' && !!last.content
})

// 检测未闭合代码围栏
function hasUnclosedCodeFence(text){
  const lines = text.split(/\n/)
  let count = 0
  for (const ln of lines){ if (/^```/.test(ln.trim())) count++ }
  return count % 2 === 1
}

function inlineLite(text){
  return text
    .replace(/&/g,'&amp;')
    .replace(/</g,'&lt;')
    .replace(/>/g,'&gt;')
    .replace(/`([^`]+)`/g,'<code>$1</code>')
    .replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>')
    .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g,'<em>$1</em>')
    .replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>')
    .replace(/\n/g,'<br/>')
}

function cursorHTML(){ return '<span class="typing-cursor"></span>' }

const streamingRendered = computed(()=>{
  const raw = props.streamingBuffer || ''
  if (!raw) return '…'
  const unclosed = hasUnclosedCodeFence(raw)
  if (!unclosed){
    return renderMarkdown(raw) + cursorHTML()
  }
  const lines = raw.split(/\n/)
  let inFence = false
  let fenceLang = ''
  let buffer = []
  const parts = []
  for (const ln of lines){
    if (/^```/.test(ln)){
      if (!inFence){
        inFence = true
        fenceLang = ln.trim().replace(/^``` ?/, '') || ''
        buffer = []
      } else {
        const codeEsc = buffer.join('\n').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
        parts.push(`<pre><code class="lang-${fenceLang}">${codeEsc}</code></pre>`)
        inFence = false
        fenceLang = ''
      }
      continue
    }
    if (inFence){
      buffer.push(ln)
    } else {
      parts.push(inlineLite(ln))
    }
  }
  if (inFence){
    const codeEsc = buffer.join('\n').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
    parts.push(`<pre class="incomplete"><code class="lang-${fenceLang}">${codeEsc}\n<span class="generating">▌</span></code></pre>`)
  }
  return parts.join('\n') + cursorHTML()
})

function renderAssistant(m){
  // 仅对已完成的 assistant 做 Markdown 渲染
  return renderMarkdown(m.content || '')
}
</script>
 <style scoped>
 .chat-message-list { flex:1; overflow-y:auto; padding:20px 26px 0; background:transparent; min-height:0; }
 .bottom-spacer { width:100%; flex-shrink:0; }
 .msg { display:flex; gap:14px; margin-bottom:18px; align-items:flex-start; }
 /* 用户消息：靠右显示（反转方向） */
 .msg.user { flex-direction: row-reverse; }
 .msg .avatar { width:38px; height:38px; border-radius:50%; background:#409eff; display:flex; align-items:center; justify-content:center; font-size:13px; color:#fff; font-weight:600; flex-shrink:0; letter-spacing:1px; }
 .msg.user .avatar { background:#67c23a; }
 .bubble-wrap { display:flex; flex-direction:column; align-items:flex-start; gap:4px; }
 .role-tag { font-size:12px; color:#6b5338; padding:2px 10px; border-radius:999px; background:linear-gradient(135deg,#f6efe3,#f1e3cf); border:1px solid rgba(200,170,120,0.6); }
 .msg .bubble { background:#f5f7fa; padding:12px 16px 14px; border-radius:14px; max-width:820px; line-height:1.6; word-break:break-word; font-size:14px; position:relative; }
 .msg.assistant .bubble { background:#eef5ff; }
 .msg.user .bubble { background:linear-gradient(135deg,#f2f9ef,#e6f4e1); }
 /* 用户消息气泡右对齐时，统一圆角（可定制尖角） */
 .msg.user .bubble { text-align:left; }
.msg.assistant.streaming .bubble { opacity:.88; }
.msg.assistant.streaming .bubble .typing-cursor { display:inline-block; width:8px; background:#888; height:1.1em; vertical-align:text-bottom; animation:blink 1s steps(2,start) infinite; margin-left:2px; border-radius:2px; }
.msg.assistant.streaming .bubble pre.incomplete { position:relative; background:#1e1e1e; color:#ddd; padding:10px 12px 12px; border-radius:8px; box-shadow:inset 0 0 0 1px #333; overflow:auto; }
.msg.assistant.streaming .bubble pre.incomplete code { font-family:Consolas,Menlo,monospace; font-size:13px; }
.msg.assistant.streaming .bubble pre.incomplete code .generating { color:#6abf4b; animation:pulse 1.2s ease-in-out infinite; }
@keyframes blink { 50% { opacity:0; } }
@keyframes pulse { 0%,100% { opacity:.25; } 50% { opacity:1; } }
 .msg .bubble :deep(p){ margin:0 0 12px; }
 .msg .bubble :deep(p:last-child){ margin-bottom:0; }
 .msg .bubble :deep(h1,h2,h3,h4){ margin:14px 0 10px; font-weight:600; line-height:1.25; }
 .msg .bubble :deep(ul){ padding-left:1.2em; margin:0 0 12px; }
 .msg .bubble :deep(li){ margin:4px 0; }
 .msg .bubble :deep(table){ border-collapse:collapse; width:100%; margin:10px 0; }
 .msg .bubble :deep(th,td){ border:1px solid #e2e8f0; padding:6px 8px; font-size:13px; }
 .msg .bubble :deep(blockquote){ margin:10px 0; padding:6px 10px; background:#f1f5f9; border-left:3px solid #90caf9; border-radius:4px; font-size:13px; color:#555; }
.chat-message-list ::v-deep pre { background:#1e1e1e; color:#ddd; padding:10px 12px; border-radius:6px; overflow:auto; }
.chat-message-list ::v-deep code { font-family:Consolas,Menlo,monospace; font-size:13px; }
.chat-message-list ::v-deep code:not(pre code) { background:#f2f2f2; padding:2px 4px; border-radius:4px; }
</style>
